feat(desktop): fold focus-mode agent work into one transcript block - #6536
feat(desktop): fold focus-mode agent work into one transcript block#6536baxen wants to merge 6 commits into
Conversation
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 643b310690
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
Latest revision and validation:
The PR description now explicitly records the intentional flat threshold change from 3 to 2 and that @ss-core-02 |
|
Validation complete at
The PR description now explicitly documents the intentional flat grouping threshold change from 3 to 2 and One independent P2 review thread remains open about orphaned executing/pending history after a crashed or disconnected session. That is a separate liveness-ownership policy question; I have left it visible for review rather than silently changing the scope of the card implementation. @ss-core-02 |
74b9015 to
e875102
Compare
e875102 to
a05347d
Compare
Revision: rebased onto #6720, and a bug the browser caughtHead: Relay posts are plain rail steps nowCaught by looking at the rendered preview, not by a unit test — the JSDOM suite was green while the rail was visibly wrong. A This is the same failure core-02 flagged for interim notes, reached by a different route: there the item is an assistant message, here it is a tool call that merely classifies as one. Fixed with an explicit presentation signal ( Before / after, same seeded turn:
Fold animation now asserted in a browserPer the quality list: the fold must animate, and a unit test can only see end states. The preview spec samples the panel height per frame while it closes and asserts at least one height strictly between full and zero — which a Mutation-checked: setting Full states
Plan sits as a sibling after the block; the failed step is inside it and named in the folded line; the answer's prose and fenced code are unaffected. Verification at
|
a05347d to
9c2900e
Compare
A tool item's `executing`/`pending` status is written when the step starts and never revised if the agent dies first, so an abandoned step keeps it forever. On the activity feed that was a stale row label. In the work block this commit introduces it is a MODE: one `running` entry makes `summarizeWorkBlock` report `isActive`, which suppresses the folded summary line, holds the rail open and pulses a bullet. Scrolling back to a crashed turn therefore showed the reader live work indefinitely (Codex on #6536, discussion_r3848214880 — not stale, and worse here than on the card it was filed against). Status alone cannot answer "is this step happening?", so the block is given the missing half: `AgentSessionTranscriptTurnMeta.liveTurnId`, published by the list from the same display blocks it already reads for `streamingItemId`. `toolEntryState` treats `executing`/`pending` as `running` only when a session owns that step's turn. A turn id, not a boolean. "Some turn is live" is not the question: an agent that crashed during turn 1 and is now working on turn 2 IS live, yet turn 1's abandoned step is no more running than before — a global flag would keep it spinning in exactly the case a restarted agent makes common. The comparison is explicit (`liveTurnId !== null && ...`) so an item with no turn id cannot match a null live turn by `null === null`. An abandoned step is reported as `settled`, not as a new state and not as `failed`, per core-02: - We do not know it failed — only that nobody finished it — so it must not count toward the folded line's `N failed`. It folds to a neutral `N steps`. - No third entry state and no new glyph. It renders as the neutral step it is, with the same muted detail the list already shows for it outside a block. A visible "interrupted" marker would be a design addition, not a bug fix. `liveTurnId` is read from the display blocks rather than from the active-turn store, because the store's turn ids and the transcript's are populated by different paths and a mismatch would silently gate every step off — the mirror image of this bug. It is the last *turn* block, not the last block: a compaction notice arrives as a `single` block after the turn it belongs to, so reading the final block's kind would report no live turn at all. `projectWorkBlockEntries` takes the option as a required argument rather than defaulting it, so a future caller that has not thought about liveness cannot silently get the spins-forever behaviour back. Non-vacuous by mutation, each mutant run in isolation against the unit suite: - gate removed (always `running`): 4 failures. - gate inverted (`executing` never `running`): 5 — this is the fix that would have "passed" the bug report while breaking live work. - truthiness instead of turn ownership (any live turn resurrects an old abandoned step): 1. - `item.turnId === liveTurnId` without the null guard: 1. - abandoned step reported `failed` instead of `settled`: 4. - `liveTurnId` forced null in the meta builder: 2. - `lastTurnId` reading only the final block: 1. Measured in a real browser as well, not only JSDOM, because the failure mode is presentational and animation-dependent — a unit test can assert class names while the rendered rail is still wrong (the interim-note and relay-send bugs on this branch were both invisible to the unit suite). Two seeded scenarios in the preview harness, driving the real component through the cover drawer under Buzz Dark: - agent panic mid-step (the terminal `crates/buzz-acp` actually emits): folded label `3 steps`, rail collapses to 0 rows, `getAnimations` reports 0 infinite animations in the block, and opened the rail reads `["settled","settled","settled"]`. - the same events with no panic: no folded line, rail open, states `["settled","settled","running"]`, and exactly 1 infinite animation — the running bullet really does pulse. Both browser scenarios were mutation-checked too: removing the gate fails the first and passes the second, inverting it fails the second and passes the first, so neither can be satisfied by a one-sided fix. (Those two specs are not in this commit — they need Slice A's `conversation` variant pin to be reachable, which is not on this branch.) The row outside a block is untouched: `buildCompactToolSummary` still derives its own `running` from status, and no file under `activityRenderClasses/` or `agentSessionToolSummary.ts` is modified here. Verified at this tree: full desktop suite 5486 passing / 0 failing, `tsc --noEmit` clean, `pnpm check` findings identical to main's baseline (2 warnings + 2 infos, all pre-existing). Co-authored-by: Bradley Axen <baxen@squareup.com> Signed-off-by: Bradley Axen <baxen@squareup.com>
440a89d to
9fb04cf
Compare
Focus mode gave every thought and tool step its own row and its own
disclosure, so a turn that did real work scrolled its answer off screen
behind a stack of chrome. This adopts berd's shipping transcript model:
within a turn, everything between the prompt and the answer that is
thinking, a tool step, or an interim agent note becomes one "work block"
— a thin rail of small glyph bullets that folds to a single "N steps"
line when the work is done.
Grouping is variant-aware AT THE LIST BOUNDARY rather than inside the
grouping module. `TranscriptDisplayBlockView` runs the additive
work-block transform only for `conversation`, so `default` and
`compactPreview` walk main's segments on the identical code path they
always did. That makes Slice B's byte-for-byte baseline fixture hold BY
CONSTRUCTION rather than by assertion, which is the property worth having
here: no future edit to the block can regress the other two variants
without first moving this branch.
Behaviour, mirroring berd:
- Live: the block is open and the rail IS the status, so there is no
header line to restate it. The last three steps show in true arrival
order; older ones go behind an "N previous steps" disclosure at the top
of the rail, so a long run cannot push the answer out of view.
- Finished: folds to "N steps" with a chevron that rotates on open.
- The fold ANIMATES. `<details>` cannot do this — its content is either
laid out or not, with no intermediate height — so the collapse would
snap. A block that was live when it mounted renders open for a paint
and then settles closed, giving the height tween a start state that was
actually painted; a block already finished on mount (scrollback) never
had a rail on screen and closes immediately, so the animation stays
meaningful instead of firing on every mount.
- Reader choice wins: once the reader toggles a block, policy stops
opening and closing it.
One deliberate departure from berd, per core-02: a finished block holding
a failure folds to "N steps · 1 failed". A bare count is the one thing a
reader cannot distinguish a clean run from a broken one by, and a fold
that hides a failure behind a neutral number invites them not to open it.
The rail bullet itself stays muted, per berd — failure is a glyph shape,
not a colour, so one bad step does not read as an alarm across the run.
Items are projected ONCE into rail entries, and the glyph, the body and
the folded line's counts all read that projection. Two independent
classifications of the same item is precisely how the headline and the
chain eligibility drifted apart on the abandoned tool-chain card, so both
render sites are exhaustive switches over the entry and neither asks
`item.type` again.
The projection is closed in the TYPE SYSTEM, not only by convention. An
earlier revision made the entry a product of independent fields
(`{ item, kind, state }`) with a catch-all `return "tool"`, which left two
things wrong that no test could see: a future `TranscriptItem` variant
would silently wear a wrench, and impossible pairs like
`{ kind: "note", item: <thought> }` stayed representable — so the body
switch still had to re-check `item.type` and render `""` on a mismatch it
could not otherwise handle. Meaning was therefore still derived in two
places.
Now:
- `WorkBlockItem` is the closed union of items a block admits, and
`isWorkItem` is a type guard, so the membership decision is made once
and every later stage receives the narrowed type. `admittedWorkItems`
returns the narrowed array rather than a boolean because an `.every()`
guard cannot narrow the array it tested.
- `WorkBlockEntry` is a discriminated union pairing each kind with its own
item type, and fixing `state: "settled"` on the prose kinds. Both classes
of impossible entry are now unrepresentable rather than defended against.
- `projectWorkBlockEntry` switches exhaustively over `WorkBlockItem` with
no default, so admitting a new item type without deciding how it renders
is a compile error (`TS2366: Function lacks ending return statement`),
not a wrench.
- The body switch takes the whole entry, so narrowing on `kind` narrows
`item` too. The `item.type === "thought" ? item.text : ""` fallbacks are
gone because there is no longer a mismatch to fall back from.
Verified by compiling three mutants, each of which now fails `tsc` where
before it type-checked: admitting `plan` to `WorkBlockItem` without a
projection case (TS2366), projecting a thought as a note (TS2322 on
`item`), and giving a thought `state: "failed"` (TS2322 on `state`). The
runtime kind/item pairing is also asserted in
`agentSessionWorkBlockGrouping.test.mjs`, because types are stripped at
runtime and swapping the two prose branches by hand is the easy mistake —
that mutant fails the test.
The projection also fixes an ordering bug the old split invited: a tool
carrying a stale `isError` from a retry while the new attempt executes
reads as `running`, not `failed`, so a live block cannot fold its own
count to "N steps · 1 failed" while the work is still in flight.
**Interim notes suppress the identity row.** #6720 gives every
conversation-variant assistant message a 20px avatar + name row, which is
right for the turn's answer. A rail note is the same item type, so
routing it through that presenter would render a fully attributed agent
turn nested inside a muted step row — the agent apparently replying twice,
once inside the work it was doing. Notes render through a dedicated rail
prose body instead, keeping markdown and the focus code-block recipe by
providing the same `CodeBlockVariantContext` value the presenter would.
Done on this side rather than by reaching into #6720, so that PR keeps one
rule for what a message looks like. Notes share the thought's speech
bubble, matching berd's `progress` entry: both are the agent talking.
- `useControlledDisclosure` is deleted rather than reused. Its entire
reason to exist was the `<details>` echo trap — `<details>` fires
`toggle` for programmatic `open` changes indistinguishably from clicks,
so a policy-driven open echoes back looking like reader intent. This
block's trigger is a `<button>`, where the only thing that can call the
handler is a real click. Keeping the guard would have been dead code
masquerading as load-bearing. Nothing else in the tree imported it.
- berd brightens rail prose with `usePrimaryText={open}`; here the
brightening is unconditional. A closed block unmounts its rows rather
than dimming them, so there is no state in which rail prose is on
screen and not in an open block — the flag's false branch would be
unreachable. A test records that reasoning so the divergence is not
mistaken for an oversight.
dev-01 flagged that `turnSegmentItems` does not know `work-block`, so a
thought followed only by block content would never settle its duration.
Tracing it: the meta is built from pre-transform display blocks, so no
`work-block` segment can reach that function — the reported bug cannot
fire. But the trace turned up something worse. `ConversationThought` was
the only reader of `thoughtDurationSecondsById` and
`formatThoughtDisclosureLabel`, and this commit deletes it, leaving ~100
lines of production code and ~200 lines of tests that only tested each
other. Fixing a bug in code with no reader would have preserved the
illusion that focus mode still shows "Thought for Ns" somewhere.
So the duration map, its label formatter and `elapsedSeconds` are gone.
`AgentSessionTranscriptTurnMeta` narrows to the one field that still has
a consumer: `streamingItemId`, which the work block needs because a
thought or note streaming in carries no status of its own. Its tests are
rewritten around what that hint must actually get right — the tail of a
live turn, skipping setup, expanding a summary segment to its last leaf,
and reporting nothing at all when the turn is idle.
Three further notes on translation rather than transcription:
- berd's bullet masks the spine with `bg-card` and its BOT-1599 note
warns off `bg-background`. The rule is "mask with the surface the
transcript is drawn on"; in Buzz that surface is the cover drawer,
which is literally `bg-background`. The two tokens are NOT
interchangeable here: `[data-buzz-content-surface]` locally overrides
`--background` to `--buzz-content-dark` while `--card` keeps the theme
value, so in Buzz Dark `bg-card` paints the bullet rgb(36,41,46) over an
rgb(26,26,26) drawer — the exact BOT-1599 failure. Light mode matches
under either class, so light-mode evidence alone would not have caught
it. Copying the class would have followed the letter of berd's note
against its point.
- Reduced motion is read via `matchMedia` — the way `TerminalSubstrate`
reads it — not motion's `useReducedMotion`, which resolves the query
once per process and caches it. That cache made the preference
untestable (the assertion turned on module load order, not on the
setting) and ignored a mid-session change.
- The memo takes the entry SPREAD into props, not the entry object, and
its boundary is the step BODY rather than the whole row. The projection
is rebuilt whenever the item array changes, so entry objects are fresh
on every append and a memo keyed on one would never hit; spread, the
compared props are `item` (reference-stable) plus two strings. Spreading
also keeps the union intact, so the body switch still narrows `item`
from `kind`. The row stays outside because the glyph depends on
`isLast`, which changes for the previous last row on every append.
`ConversationThought` is deleted here rather than in dev-01's restyle,
per core-02's sequencing ruling: this is the commit that replaces it, so
reasoning stays visible in focus mode at every commit. Its four
disclosure-keyed tests are deleted rather than adapted — they assert a
`<details>` that no longer exists on that path. `default`/
`compactPreview` thought rendering is untouched.
Non-vacuous by mutation testing, each mutant run in isolation:
- note falls through to the tool kind: 4 failures.
- note routed through the message presenter: 2. Note given the wrench: 1.
- `entryState` checking failure before running: 1.
- rail bullet tinted on failure: 1. Prose muted instead of primary: 1.
- memo keyed on the projected entry object: 2 — this is the bug the
projection actually introduced, caught by the pre-existing streaming
cost tests before it shipped.
- streaming hint forced to null: 4. Summary segments not expanded when
finding the tail: 1.
- windowing keyed off `open` instead of the reader's choice: 3. Policy
already holds live blocks open, so this switches windowing off in
exactly the case it exists for.
- reduced-motion preference forced false: 1.
- rail bubble suppression removed (the bug above): the relay-step test
stops passing.
An earlier mutant also caught a test lying: the block-level "echo" test
passed with the guard removed, because the trigger is a `<button>` and
nothing was listening for `toggle` at all. It now asserts the structural
reason the trap cannot apply plus a repeated fold→reopen→fold cycle,
which is what a recorded echo would actually have disabled.
Relay posts are plain rail steps. A `buzz messages send` step classifies
as `renderClass: "message"`, so it renders through `CompactMessageSummary`
— 28px avatar, bordered speech bubble, timestamp, delivery-receipt
button. Correct in the activity feed, where a posted message is a
destination to open; on a muted rail it makes the agent appear to reply
in the middle of its own work. This is the same failure the interim-note
case avoids, reached by a different route: there the item IS an assistant
message, here it is a tool call that merely classifies as one. Caught by
looking at the seeded browser preview, not by a unit test — the JSDOM
suite was green while the rendered rail was wrong.
Suppressed with an explicit presentation signal
(`useIsInsideWorkBlockRail`, default false) rather than by reading the
transcript variant, because `conversation` alone is not the condition:
the same relay step rendered OUTSIDE a block in that variant should keep
its bubble. Both halves of that branch are now pinned by tests, so
suppressing the bubble everywhere cannot pass. Defaulting to false keeps
`default`/`compactPreview` markup byte-identical.
Rebased onto #6720 at `e8709554a` per core-02's B → #6720 → C ordering.
Verified at this tree: full desktop suite 5472 passing / 0 failing,
`tsc --noEmit` clean, `pnpm check` findings identical to main's baseline
(2 warnings + 2 infos, all pre-existing), px-text/pubkey-truncation/
file-size gates clean.
Co-authored-by: Bradley Axen <baxen@squareup.com>
Signed-off-by: Bradley Axen <baxen@squareup.com>
A tool item's `executing`/`pending` status is written when the step starts and never revised if the agent dies first, so an abandoned step keeps it forever. On the activity feed that was a stale row label. In the work block this commit introduces it is a MODE: one `running` entry makes `summarizeWorkBlock` report `isActive`, which suppresses the folded summary line, holds the rail open and pulses a bullet. Scrolling back to a crashed turn therefore showed the reader live work indefinitely (Codex on #6536, discussion_r3848214880 — not stale, and worse here than on the card it was filed against). Status alone cannot answer "is this step happening?", so the block is given the missing half: `AgentSessionTranscriptTurnMeta.liveTurnId`, published by the list from the same display blocks it already reads for `streamingItemId`. `toolEntryState` treats `executing`/`pending` as `running` only when a session owns that step's turn. A turn id, not a boolean. "Some turn is live" is not the question: an agent that crashed during turn 1 and is now working on turn 2 IS live, yet turn 1's abandoned step is no more running than before — a global flag would keep it spinning in exactly the case a restarted agent makes common. The comparison is explicit (`liveTurnId !== null && ...`) so an item with no turn id cannot match a null live turn by `null === null`. An abandoned step is reported as `settled`, not as a new state and not as `failed`, per core-02: - We do not know it failed — only that nobody finished it — so it must not count toward the folded line's `N failed`. It folds to a neutral `N steps`. - No third entry state and no new glyph. It renders as the neutral step it is, with the same muted detail the list already shows for it outside a block. A visible "interrupted" marker would be a design addition, not a bug fix. `liveTurnId` is read from the display blocks rather than from the active-turn store, because the store's turn ids and the transcript's are populated by different paths and a mismatch would silently gate every step off — the mirror image of this bug. It is the last *turn* block, not the last block: a compaction notice arrives as a `single` block after the turn it belongs to, so reading the final block's kind would report no live turn at all. `projectWorkBlockEntries` takes the option as a required argument rather than defaulting it, so a future caller that has not thought about liveness cannot silently get the spins-forever behaviour back. Non-vacuous by mutation, each mutant run in isolation against the unit suite: - gate removed (always `running`): 4 failures. - gate inverted (`executing` never `running`): 5 — this is the fix that would have "passed" the bug report while breaking live work. - truthiness instead of turn ownership (any live turn resurrects an old abandoned step): 1. - `item.turnId === liveTurnId` without the null guard: 1. - abandoned step reported `failed` instead of `settled`: 4. - `liveTurnId` forced null in the meta builder: 2. - `lastTurnId` reading only the final block: 1. Measured in a real browser as well, not only JSDOM, because the failure mode is presentational and animation-dependent — a unit test can assert class names while the rendered rail is still wrong (the interim-note and relay-send bugs on this branch were both invisible to the unit suite). Two seeded scenarios in the preview harness, driving the real component through the cover drawer under Buzz Dark: - agent panic mid-step (the terminal `crates/buzz-acp` actually emits): folded label `3 steps`, rail collapses to 0 rows, `getAnimations` reports 0 infinite animations in the block, and opened the rail reads `["settled","settled","settled"]`. - the same events with no panic: no folded line, rail open, states `["settled","settled","running"]`, and exactly 1 infinite animation — the running bullet really does pulse. Both browser scenarios were mutation-checked too: removing the gate fails the first and passes the second, inverting it fails the second and passes the first, so neither can be satisfied by a one-sided fix. (Those two specs are not in this commit — they need Slice A's `conversation` variant pin to be reachable, which is not on this branch.) The row outside a block is untouched: `buildCompactToolSummary` still derives its own `running` from status, and no file under `activityRenderClasses/` or `agentSessionToolSummary.ts` is modified here. Verified at this tree: full desktop suite 5486 passing / 0 failing, `tsc --noEmit` clean, `pnpm check` findings identical to main's baseline (2 warnings + 2 infos, all pre-existing). Co-authored-by: Bradley Axen <baxen@squareup.com> Signed-off-by: Bradley Axen <baxen@squareup.com>
A tool item's `executing`/`pending` status is written when the step starts and never revised if the agent dies first, so an abandoned step keeps it forever. On the activity feed that was a stale row label. In the work block this commit introduces it is a MODE: one `running` entry makes `summarizeWorkBlock` report `isActive`, which suppresses the folded summary line, holds the rail open and pulses a bullet. Scrolling back to a crashed turn therefore showed the reader live work indefinitely (Codex on #6536, discussion_r3848214880 — not stale, and worse here than on the card it was filed against). Status alone cannot answer "is this step happening?", so the block is given the missing half: `AgentSessionTranscriptTurnMeta.liveTurnId`, published by the list from the same display blocks it already reads for `streamingItemId`. `toolEntryState` treats `executing`/`pending` as `running` only when a session owns that step's turn. A turn id, not a boolean. "Some turn is live" is not the question: an agent that crashed during turn 1 and is now working on turn 2 IS live, yet turn 1's abandoned step is no more running than before — a global flag would keep it spinning in exactly the case a restarted agent makes common. The comparison is explicit (`liveTurnId !== null && ...`) so an item with no turn id cannot match a null live turn by `null === null`. An abandoned step is reported as `settled`, not as a new state and not as `failed`, per core-02: - We do not know it failed — only that nobody finished it — so it must not count toward the folded line's `N failed`. It folds to a neutral `N steps`. - No third entry state and no new glyph. It renders as the neutral step it is, with the same muted detail the list already shows for it outside a block. A visible "interrupted" marker would be a design addition, not a bug fix. `liveTurnId` is read from the display blocks rather than from the active-turn store, because the store's turn ids and the transcript's are populated by different paths and a mismatch would silently gate every step off — the mirror image of this bug. It is the last *turn* block, not the last block: a compaction notice arrives as a `single` block after the turn it belongs to, so reading the final block's kind would report no live turn at all. `projectWorkBlockEntries` takes the option as a required argument rather than defaulting it, so a future caller that has not thought about liveness cannot silently get the spins-forever behaviour back. Non-vacuous by mutation, each mutant run in isolation against the unit suite: - gate removed (always `running`): 4 failures. - gate inverted (`executing` never `running`): 5 — this is the fix that would have "passed" the bug report while breaking live work. - truthiness instead of turn ownership (any live turn resurrects an old abandoned step): 1. - `item.turnId === liveTurnId` without the null guard: 1. - abandoned step reported `failed` instead of `settled`: 4. - `liveTurnId` forced null in the meta builder: 2. - `lastTurnId` reading only the final block: 1. Measured in a real browser as well, not only JSDOM, because the failure mode is presentational and animation-dependent — a unit test can assert class names while the rendered rail is still wrong (the interim-note and relay-send bugs on this branch were both invisible to the unit suite). Two seeded scenarios in the preview harness, driving the real component through the cover drawer under Buzz Dark: - agent panic mid-step (the terminal `crates/buzz-acp` actually emits): folded label `3 steps`, rail collapses to 0 rows, `getAnimations` reports 0 infinite animations in the block, and opened the rail reads `["settled","settled","settled"]`. - the same events with no panic: no folded line, rail open, states `["settled","settled","running"]`, and exactly 1 infinite animation — the running bullet really does pulse. Both browser scenarios were mutation-checked too: removing the gate fails the first and passes the second, inverting it fails the second and passes the first, so neither can be satisfied by a one-sided fix. (Those two specs are not in this commit — they need Slice A's `conversation` variant pin to be reachable, which is not on this branch.) The row outside a block is untouched: `buildCompactToolSummary` still derives its own `running` from status, and no file under `activityRenderClasses/` or `agentSessionToolSummary.ts` is modified here. Verified at this tree: full desktop suite 5486 passing / 0 failing, `tsc --noEmit` clean, `pnpm check` findings identical to main's baseline (2 warnings + 2 infos, all pre-existing). Co-authored-by: Bradley Axen <baxen@squareup.com> Signed-off-by: Bradley Axen <baxen@squareup.com>
9fb04cf to
e9e2db4
Compare
Final verification at
|
|
Rebased final head verified: |
e9e2db4 to
fe57b7a
Compare
|
Final C head is I re-read the final conversation test file on this head and confirmed the coverage remains in the owning files: main conversation contract 12 tests / 366 lines; chrome 9 tests; shared harness 576 lines. The existing work-block, grouping, and metadata suites remain 30/30, 26/26, and 13/13. Full pre-push desktop test, typecheck, check, and push all passed. PR CI run The over-ceiling check was run against #6736's |
|
C's pushed head is The re-homed conversation test file is 366 lines; chrome is 274 and the shared harness is 576. The work-block test remains 1,145 lines and is intentionally the post-bug-pass split item, because the The Codex thread is still intentionally unresolved. It will get one final reply for |
…harness `AgentSessionWorkBlock.test.mjs` landed at 1,146 lines. Under today's rule tables that passes, but only because the desktop ratchet's script roots allowlist `.ts`/`.tsx` and silently skip `.mjs` — the gap #6736 (`ce1f1d427`) closes. Measured rather than assumed: with that rule table cherry-picked, this file is the one remaining violation on C, and because `allowedLineCount` grandfathers a base that already exceeds the max, whatever count C lands with becomes the file's permanent ceiling on main. 1,146 is not a ceiling worth inheriting for a file that exists to cover a single component. Split on the seam the file already had, at `// -- Orphaned work --`, where the subject changes from live-vs-finished *policy* to what an individual *row* is: - `AgentSessionWorkBlockTestRig.mjs` (311) — jsdom lifecycle, the item fixtures, `settle()`, `renderBlock()`. - `AgentSessionWorkBlock.test.mjs` (352) — Live, Finished, fold animation, reader choice, rail glyph states. - `AgentSessionWorkBlock.orphaned.test.mjs` (528) — orphaned work, the per-kind rail presentation, streaming re-render cost. One rig, not a copy per file. The two suites run in separate processes (node's runner is one process per file), so a second copy of the jsdom and `matchMedia` setup could not *collide* — it would drift, and a drifted ambient pin fails a fixture for a reason that has nothing to do with the markup under test. That trap already cost this suite family two commits (`2b7b0baf6`, `c91819706`). The split forces one real behavioural change. `prefersReducedMotion` was a module-level `let` the reduced-motion test assigned directly; ESM bindings are read-only in importers, so once it lives in the rig that assignment cannot work. It goes through `setPrefersReducedMotion(value)`, with the rig's `afterEach` still resetting it. Proved non-vacuous by stubbing the setter to a no-op: that fails exactly one test — the reduced-motion one — and restoring it passes. Without that check the test would have been asserting against a flag nothing sets, which is precisely the way a split can silently disarm a test. Behaviour is otherwise preserved, checked rather than assumed: the 30 test titles across the two files are an exact set match with the 30 in the single file, and every body is byte-identical apart from the setter call. Harness prune, in the same revision because it is the same debt: with the four `<details>` disclosure tests gone, `export const domWindow = dom.window` had no consumer anywhere in `desktop/src` — it existed only for `new domWindow.Event("toggle")`. Dropping just the `export` leaves an unused local that Biome flags (`lint/correctness/noUnusedVariables`), so the declaration and its now-false doc comment both go (576 -> 571). `TRIGGER_TITLE` only loses its `export`; it is still used inside the harness. Ratchet with #6736's rules cherry-picked, run in the same shell as `git rev-parse HEAD`: - base `fd2e01799` (the current PR base, which is what CI resolves — `resolveBaseRef` returns `HEAD^1` under `GITHUB_ACTIONS`): exit 0. - base `merge-base(origin/main)` = `db5617dd1`, which is what it resolves to locally and would resolve to if C retargets: exit 0. - negative control at the pre-split tree, same rules, same base: exit 1, `AgentSessionWorkBlock.test.mjs: new -> 1146 lines (allowed 1000)`. The gate is measuring the thing this commit fixes. Both suites green together, 30/30. `conversation` + `conversationChrome` still green. `tsc --noEmit` clean, `pnpm check` findings identical to main's baseline. The split shape, the seam and the reduced-motion proof are dev-01's, handed over as a verified patch (`OUTBOX/C_WORKBLOCK_SPLIT_HARNESS_PRUNE.patch`, sha256 6cfbccc9…) rather than as a commit in this checkout. I read the seam, reconstructed the tree from `fe57b7ac7` + that patch to confirm it is exactly what was proposed, and re-ran the title/body comparison, the setter mutant, both ratchet bases and the negative control here. Co-authored-by: Bradley Axen <baxen@squareup.com> Signed-off-by: Bradley Axen <baxen@squareup.com>
9d39bd2 to
036bd38
Compare
…ing live Two reader-visible bugs in the focus-mode work block, both found reviewing this PR rather than by a test. **The running step's pulse ignored reduced motion.** `WorkBlockRailGlyph` applied bare `animate-pulse`. `useWorkBlockMotionEnabled` skips the fold's height animation, but it cannot reach a keyframe animation applied by a utility class, and none of the app's 20 `prefers-reduced-motion: reduce` blocks matches `.animate-pulse` — every one is scoped to a `buzz-*`/`motion-*`/`t-skel-*` class. So a reader who asked for no motion got an indefinite pulse. Now `motion-safe:animate-pulse`, which this repo's Tailwind (4.3.0) compiles to the same declaration wrapped in `@media (prefers-reduced-motion: no-preference)` — and which matches every other pulse in this feature (`AgentStatusBadge`, `ManagedAgentRow`). That swap also repaired three assertions it would otherwise have disarmed. `motion-safe:animate-pulse` is a different class TOKEN, not `animate-pulse` plus a modifier, so the suite's `.animate-pulse` selectors would have stopped matching anything and passed unconditionally. Worse, they were already vacuous: all three ran after `settleToStepCount(0)`, i.e. on a folded rail with no rows, so "nothing pulses" held against a build where abandoned steps pulse forever — the exact Codex finding this PR answers. They now assert on rows that exist (expanded, or mid-settle) and match the exact class token, so neither a substring nor a missing element can fake a pass. **A finished block went live again between turns.** `lastTurnId` walked the display blocks for the newest `turn` block, but a turn that has only emitted setup lifecycle rows (`turn_started`, `session_resolved`) classifies to zero segments and so produces no block at all. For the whole gap between `turn_started` and the next turn's first renderable item, "newest turn with a block" was therefore the turn that had already ENDED, and its own trailing item became the streaming item: a settled 6-step block re-opened, dropped to its last three steps behind a previous-steps disclosure, then folded back. `turn_started` fires on every turn and the gap is real observer-stream latency, so this was every turn, not an edge case. Liveness now comes from the newest turn id in the item stream, which is why `buildConversationTurnMeta` takes `items`. A trailing item is also only reported as streaming when the live turn owns it — the same ownership rule `toolEntryState` already applies to an `executing` status, for the same reason: without it the newest block's tail holds a finished turn's block open regardless of whose turn it is. Tests, each mutation-checked in isolation so none of them is vacuous: - pulse guarded, asserted under BOTH preference values (the class must not depend on it) — reverting to the bare class fails exactly that test; dropping the `running` guard fails the three retargeted negatives. - the meta gap contract over the real 4-frame sequence built from a raw item stream through the real grouping, since the bug is precisely which turns do and do not produce a block — reverting either half of the fix fails it. - the rendered consequence with a 6-step block (above the live window, where the symptom is loudest): folded summary and no rail across the gap. Also fixed a fixture that named the wrong thing: "no turn at all" carried the shared helper's default `turnId: "turn-1"`, so it was asserting that a turn with no block reports no live turn — the opposite of the rule. Full desktop suite 5491 passing / 0 failing, 81 suites; `tsc --noEmit` clean. Co-authored-by: Bradley Axen <baxen@squareup.com> Signed-off-by: Bradley Axen <baxen@squareup.com>
…e hang
Two review corrections to the previous commit, neither changing production
behaviour.
**The gap test was pinning the wrong sequence.** Its middle frames put a
`session/new` card between `turn_started` and `session_resolved`. That card
renders as a trailing `single` block, which moves `streamingItemId` off the
previous turn's work block on its own — so those frames pass against the old
block-walking code and prove nothing. Measured over the revert mutation:
SEQ A f1 turn_started only liveTurnId=turn-1 tail=tool:2 BUG
SEQ A f2 +session_resolved, no session/new liveTurnId=turn-1 tail=tool:2 BUG
SEQ B f1 +session/new liveTurnId=turn-1 tail=null masked
SEQ B f2 +session/new +session_resolved liveTurnId=turn-1 tail=null masked
The plain sequence — no session restart, which is the overwhelmingly common
path — now leads the frame list, and the restart sequence is kept as a
separately labelled path that also has to settle. Reverting either half of the
fix now fails on frame 1 of the plain sequence rather than on a frame that a
restart card was carrying.
**A five-minute gc timer was being waited out on every run.** The orphaned
suite's own `QueryClient` took React Query's default 300000ms `gcTime`. The one
test that reaches the message-bubble presenter's `useQuery` leaves a query
uncollected, so a 300s timer is armed at teardown and node:test waits it out
before exiting: the file's tests sum to ~2s, the wall was ~303s, and every test
passed, so nothing pointed at the cause. `gcTime: 0` takes the standalone file
from 303s to 7s, and the full desktop suite from 307s to 102s.
The shared rig (`AgentSessionWorkBlockTestRig`) creates a byte-identical client
and gets the same line. It is dormant today — only the relay-bubble test drives
a `useQuery`, and that test builds its own client — but the failure mode if it
ever arms is a silent five-minute hang with no failing assertion, which is
expensive to diagnose and free to prevent.
Verified with a negative control rather than on the passing tree alone: with the
orphaned file restored to its previous blob (17ce5d6) the same command in the
same shell takes 303s, so the number is attributable to the one line.
Contrary to the review note that prompted this, the
'Promise resolution is still pending' marker is NOT present on the previous
commit's tree — grep counts 0 both with and without the fix — so it is not
claimed as fixed here.
Full desktop suite 5491 passing / 0 failing, 81 suites; `tsc --noEmit` clean.
Co-authored-by: Bradley Axen <baxen@squareup.com>
Signed-off-by: Bradley Axen <baxen@squareup.com>
…roups it
A reader who opened a finished work block to read its steps was folded shut by
the agent posting another message — an event they did not cause and could not
predict.
Work blocks are derived, not stored. `groupConversationWorkBlocks` rebuilds them
every render and ids each one after its first step, and `findFinalAnswerId`
exempts only the LAST assistant message from the block. So a second assistant
message demotes the first: the earlier answer becomes work, and the runs on
either side of it merge.
frame 2 work-block:th:1[th:1,tool:1] msg:1 work-block:th:2[th:2,tool:2]
frame 3 work-block:th:1[th:1,tool:1,msg:1,th:2,tool:2] msg:2
`work-block:th:2` stops existing, React unmounts it, and the `useState` holding
the reader's expansion goes with it. The block they were reading is still on
screen — it is just inside a different block now — so this is not a case where
the intent has nowhere to live.
The choice therefore moves above the block, keyed by the STEP ids it was taken
on rather than by block id, since the steps are what the reader's intent was
actually about and they survive regrouping. Two rules follow from the merge:
- **An open choice wins over a folded one.** A merged block can carry both, and
the two are not symmetric: showing steps a reader did not ask for costs them a
scroll, hiding steps they did ask for loses what they were reading.
- **A choice is recorded against every step in the block, not just the first.**
Recording only the first leaves the absorbed block's stale `open` entry behind,
and since open wins the read, the merged block could never be folded again —
the reader's click would do nothing. Caught by mutation, not by inspection; the
first version of the test passed against it, so it now asserts the fold-back.
`useWorkBlockDisclosureStore` returns `null` rather than a no-op store when no
transcript provides one, and the block falls back to local state. A no-op default
would make a block mounted on its own silently swallow every click — how most of
this component's tests mount it — and local state is the correct behaviour in
isolation, where nothing is regrouping anything. A provider emits no DOM, so
`default`/`compactPreview` markup stays byte-identical (the byte-for-byte
baseline test covers this).
Verified by mutation, each applied and reverted in isolation:
- bypass the shared store (i.e. the pre-fix behaviour) — the new test fails on
the merge frame, `0 !== 1` open blocks, which is exactly the reader-visible
symptom.
- record the choice against only the first step — fails the fold-back
assertion, `1 !== 0`.
The test drives the real `AgentSessionTranscriptList` across the three frames so
the actual regrouping runs, and asserts the steps are on screen rather than
trusting a block that reports itself open.
Full desktop suite 5492 passing / 0 failing, 81 suites; `tsc --noEmit` clean.
Co-authored-by: Bradley Axen <baxen@squareup.com>
Signed-off-by: Bradley Axen <baxen@squareup.com>
|
Superseded by #6911, which consolidates the full agent-activity focus-view stack (plus tho's polish) into one PR against current main, per baxen's call. No further changes will land here. |





Retargeted: the work block, not the tool-chain card
This PR previously carried Slice C's tool-chain card presentation. Per ss-core-02's direction change (2026-08-24), that presentation is superseded by berd's shipping transcript model, and this same PR is retargeted rather than replaced.
ToolChainCards.tsx, the derived verb/object headline, and the card chrome are gone. Carried forward: the grouping module, step memoization, and their tests. (TheuseControlledDisclosurehook was carried at first, then deleted — see below.)Head:
036bd38c3bd9dd9ea0b4652ec30e5c4609f36f6d, three commits on top of #6720's current headfd2e017998c666ccd520a0b430a3db881e32401a. Rebased ontoss-dev-01/berd-restyle(#6720) per core-02's revised ordering: B (#6538) → #6720 → C (this PR). Both must merge first.Focus mode gave every thought and tool step its own row and its own disclosure, so a turn that did real work scrolled its answer off screen behind a stack of chrome. Now, within a turn, everything between the prompt and the final answer that is thinking, a tool step, or an interim agent note becomes one work block — a thin rail of small glyph bullets that folds to a single
N stepsline when the work is done.Behaviour
N previous stepsdisclosure at the top.6 steps · 1 failed), chevron right, rotating down on open. Clicking expands the whole rail. The fold animates: the block mounts open and settles closed after a paint, matching berd. Native<details>cannot animate height, so this usesmotion(already a dep).window.matchMedia("(prefers-reduced-motion: reduce)")with achangelistener, followingTerminalSubstrate.tsx. Note for reviewers:motion's ownuseReducedMotioncaches the media query at import andMotionConfig reducedMotiondoes not override it; verified empirically with throwaway probes before choosing this route.size-5glyph bullets that mask the spine. Thought and interim-note rows get a speech-bubble glyph, tool rows a wrench, running steps pulse. Failure is a glyph shape, not a colour, so one failed step does not read as an alarm across the whole run; the tinted output block carries the red when expanded.$#6834 — a relay-post rail step whose--contentcontains a backtick or$gets anullpreview from the classifier, and the rail (unlike the message bubble) has no event-fetch fallback, so it renders as a bareSent messagesrow. Not fixed here; disposition waits on the rail-step-vs-message design call.Grouping is variant-aware AT THE LIST BOUNDARY
TranscriptDisplayBlockViewruns the additive work-block transform only forconversation:defaultandcompactPreviewtherefore walk main's segments on the identical code path they always did, which makes Slice B's byte-for-byte baseline fixture hold by construction rather than by assertion. This is cleaner than Slice C's approach of special-casing the preview inside the component.One projection per leaf: a closed entry type
Every admitted leaf is projected once into a rail entry, and the glyph, the body, and the folded line's counts all read that projection instead of re-asking
item.type === ...at each render site. That re-derivation is what made an interim agent note fall through to the tool branch and pick up a wrench (ss-quality-00's finding 1).The model is a discriminated union, not a
{ item, kind, state }product:Three things stop compiling as a result, each verified with a throwaway mutant rather than asserted:
TS2366—projectWorkBlockEntrylacks an ending return statementnoteTS2322—rolemissing (WorkBlockNoteItemisExtract<TranscriptItem, {type:"message"}> & { role: "assistant" })failedTS2322—"failed"is not assignable to"settled"Supporting changes that make the union reach the render sites:
isWorkItemis a type guard, andadmittedWorkItems(segment, finalAnswerId)returnsWorkBlockItem[] | nullinstead of a boolean. An.every()predicate cannot narrow the array it just tested, so the old boolean form lost the type at the admission boundary.projectWorkBlockEntryis an exhaustive switch with nodefault. The repo has precedent forconst x: never = ...exhaustiveness checks (sound.ts,SettingsPanels.tsx), but for a returning function the missing-return error is stronger and needs no extra code.toolEntryStatechecks running before failed, so a staleisErrorfrom a retry cannot fold a live block toN steps · 1 failedwhile the work is still in flight.WorkBlockStepBody's props rather than passed as anentryobject. The projection is rebuilt on every append, so entry objects are fresh each time and a memo keyed on one would never hit; spread, the compared props are the reference-stableitemplus two strings.kindnarrowsitem, which deleted the previousitem.type === "thought" ? item.text : ""re-checks. Those silent empty-body branches are now unrepresentable rather than merely unreached.TypeScript is stripped at runtime in this repo's
.mjstests, so the compile-time half is enforced by production code plustscand the runtime half by an explicit[kind, item.type]pairing assertion inagentSessionWorkBlockGrouping.test.mjs. Non-vacuity checked by swapping the two prose projection branches — the test fails; restored, it passes.Decisions worth reviewing as decisions
Block id derives from the FIRST item (
work-block:${first.id}). Keying on the last item would remount the block on every streamed append and discard the reader's disclosure choice.Maximal runs of consecutive work items, not "everything between prompt and answer." When a non-work row (permission gate, error, mid-turn plan update) lands inside the work, the span reading would have to lift that row out of position. Splitting keeps every row where it happened — which matters most for exactly those rows.
Summary segments are expanded back to leaf tool rows inside the block, so the reader never faces two collapsed layers. The block is the one grouping in this variant.
The final answer is identified positionally (last assistant message), not by liveness, so block membership does not reshuffle at turn completion.
formatWorkBlockSummaryLabeldeparts from berd by appending· N failed. A bare step count is the one thing that leaves a reader unable to tell a clean run from a broken one.isActiveaccepts two evidence sources — a step reporting itself running, and the list'sstreamingItemIdhint. Either alone leaves a gap: a streaming thought carries no tool status, and a tool left executing after an observer-stream drop would pin the block open forever.bg-background, not berd's literalbg-card, for the bullet mask. Same rule, different surface: in berd the transcript sits on a card, in Buzz it sits on the drawer'sbg-background. The two tokens are not interchangeable here — in Buzz Dark the drawer sits inside[data-buzz-content-surface], which locally overrides--backgroundto--buzz-content-darkwhile--cardkeeps the theme value. Measured in a seeded browser,bg-cardpaints the bulletrgb(36,41,46)over argb(26,26,26)drawer: a visible disc of the wrong shade, which is exactly the BOT-1599 failure berd's note warns about.Interim notes and relay posts are suppressed on THIS side, via a dedicated prose body and an explicit
useIsInsideWorkBlockRailsignal, rather than by reaching into the message presenter — so style(desktop): bring the conversation variant closer to berd's recipes #6720 keeps one rule for what a message looks like. Two different routes reach the same wrong result: an interim note is an assistant message (style(desktop): bring the conversation variant closer to berd's recipes #6720 would give it a 20px avatar + name identity row), and a relaymessages sendstep is a tool call that merely classifies asrenderClass: "message"(which routes it to a 28px avatar + speech bubble + delivery receipt). Either one nested in a muted rail step reads as the agent replying inside its own work. The signal defaults tofalse, so the other two variants cannot observe it.The signal is presentation, not variant.
conversationalone is not the condition — the same relay step rendered outside a block in that variant should keep its bubble, and a test pins that half of the branch so suppressing it everywhere cannot pass.ConversationThoughtremovalPer ss-core-02's sequencing ruling (b) and ss-dev-01's handoff, the
ConversationThoughtbranch ofactivityRenderClasses/ThoughtActivity.tsxis deleted in this PR, because this is the commit where the rail starts rendering thinking as a row — so no commit ever leaves focus mode with reasoning invisible. Thedefault/compactPreviewthought path is untouched. The four conversation-variant thought tests keyed to the old<details>are deleted rather than adapted, since the element they assert no longer exists on that path.Files
Added:
agentSessionWorkBlockGrouping.ts—groupConversationWorkBlocks,conversationSegmentsForBlock,projectWorkBlockEntries,summarizeWorkBlock,formatWorkBlockSummaryLabel,formatPreviousStepsLabel,windowWorkBlockEntries,WORK_BLOCK_LIVE_WINDOW_SIZE = 3, and theWorkBlockItem/WorkBlockEntrytypesAgentSessionWorkBlock.tsx— the rail UI +AgentSessionWorkBlockSegmentagentSessionWorkBlockGrouping.test.mjsAgentSessionWorkBlockTestRig.mjs(311) — shared jsdom lifecycle, item fixtures andrenderBlockfor the two work-block suitesAgentSessionWorkBlock.test.mjs(352) — live, finished, fold animation, reader choice, rail glyph statesAgentSessionWorkBlock.orphaned.test.mjs(528) — orphaned work, per-kind rail presentation, streaming re-render costModified:
AgentSessionTranscriptList.tsx(variant branch,work-blocksegment kind),ThoughtActivity.tsx,AgentSessionTranscriptList.conversation.test.mjs,AgentSessionTranscriptList.conversationHarness.mjs(dead-export prune),agentSessionTranscriptContext.ts(the rail presentation signal),AgentSessionToolItem/ToolItem.tsx(honours it),agentSessionConversationMeta.ts.Deleted as dead code, each with a comment or test recording why:
shared/hooks/useControlledDisclosure.ts+ test — the block's trigger is a<button>, so there is no browsertoggleecho to guard against and the hook had no remaining consumer.thoughtDurationSecondsById,formatThoughtDisclosureLabel,elapsedSeconds+ ~200 lines of tests that only tested themselves.ConversationThoughtwas their only reader, and this PR deletes it. A bug had been reported in that code; fixing dead code would have been worse than removing it.Verification
Gates at
036bd38c3: desktop suite 5487 passing / 0 failing (81 suites),tsc --noEmitclean,pnpm checkat main's exact 4-finding baseline (2 warnings + 2 infos, all pre-existing, checked against main rather than assumed), px-text / pubkey-truncation / file-size gates clean,git diff --checkclean. Focused suites: grouping + conversation-meta 39/39, the two work-block suites 30/30, conversation + chrome 21/21. CI on this head: 14 pass, 9 skipped, 0 failing,mergeStateStatus: CLEAN.Eleven runtime mutants, each run in isolation, all caught: interim note falling through to the tool kind (4 failures),
toolEntryStateordering failed-before-running (1), note through the message presenter (2), note given a wrench (1), bullet tinted red (1), prose muted (1), memo keyed on the freshly-projected entry object (2 — the actual bug I hit), streaming hint forced null (4), summary segments not expanded when finding the tail (1), rail bubble suppression removed (the relay-step test stops passing), and swapping the two prose projection branches (the[kind, item.type]pairing assertion fails). Plus the three compile-time mutants in the entry-type table above. Tree restored and re-verified afterward.The fold animation is asserted in a real browser, not just at its end states — the preview spec samples the panel height per frame while it closes and requires at least one height strictly between full and zero. Re-run against this head's production tree:
fold heights: 245.5 -> 0 via 41 samples, passing. Non-vacuity re-confirmed at this head by settingCOLLAPSE_TRANSITION.durationto0: the run fails with "the fold must pass through intermediate heights — a details element would jump straight to 0". A<details>element cannot animate height and fails the same way. The reduced-motion endpoint is covered in the same spec.One honest note on coverage: mutating the echo guard revealed that a block-level echo test I had written was vacuous — the block's disclosure is a
<button>, not<details>, so there is no programmatic toggle to echo. The guard and its hook were deleted as dead code rather than kept with a passing-but-empty test.Screenshots
Seeded through the real
__BUZZ_E2E_SEED_OBSERVER_EVENTS__observer-frame path. No production caller passesvariant="conversation"yet — the cover drawer that pins it is ss-dev-00's separate slice — so the variant was pinned in a throwaway worktree with Slice A cherry-picked to capture these. The work block is not reachable in a build until that slice lands alongside this one. The preview spec is not committed to this branch.Provenance of the browser numbers, stated exactly because a previous revision of this description got it wrong: the preview tree is not a checkout of this branch (it carries the
conversationvariant pin and unrelated main drift), so "same head" is the wrong claim to make about it. What is checked instead is that the five production files this PR touches are byte-identical there to their blobs at036bd38c3—AgentSessionWorkBlock.tsx27055c199,agentSessionWorkBlockGrouping.ts9d076f84f,agentSessionConversationMeta.ts645622598,agentSessionTranscriptContext.tsaab5ed1d2,MessageActivity.tsx9b1f8d588— verified bygit hash-objectagainstgit rev-parse 036bd38c3:<path>in the same shell as the run. The earlier attribution toa05347dd0was doubly wrong: that head predates the orphan gate entirely (liveTurnIddoes not appear in its grouping or meta blob), and the preview tree was carryingMessageActivity.tsxat the pre-#6720 blob6b42a637arather than either head's. The 3-spec run is green on the corrected tree.7 steps · 1 failed)The rail at review size — every step reads the same way, including the relay post (
Sent Confirmed the plural/singular mismatch…), which earlier rendered as a speech bubble with an avatar and delivery receipt:Measured bullet/surface colours, both themes:
rgb(255,255,255)rgb(255,255,255)rgb(229,229,230)rgb(26,26,26)rgb(26,26,26)rgb(64,69,74)Exact match in both, and asserted (
expect(bullet).toBe(drawer)) rather than eyeballed, so the BOT-1599 masking contract holds. Re-run against this head's production tree in buzz-dark:{"bullet":"rgb(26, 26, 26)","drawer":"rgb(26, 26, 26)","spine":"rgb(64, 69, 74)"}, passing. Non-vacuity re-confirmed at this head by swapping the bullet tobg-card:Expected: "rgb(26, 26, 26)" / Received: "rgb(36, 41, 46)"— exactly the wrong-shade disc berd's note warns about.Screenshot hosting: these are
raw.githubusercontent.comURLs fromscripts/post-screenshots.sh. The previousbuzz.block.builderlab.xyz/media/...links returned 401 to GitHub's anonymous camo proxy and rendered broken for anyone reading on GitHub.Orphaned running steps
The independent bug pass reproduced a C-specific wrong state: reopened history with an
executing/pendingtool and no live session stayed expanded, pulsed indefinitely, and engaged the live window. Fixed infe57b7ac7.AgentSessionTranscriptTurnMetanow carries the channel-scoped live turn id; in-flight tool entries arerunningonly when their item turn matches it. Abandoned in-flight statuses project to neutralsettledfor policy, so history folds toN stepswithout inventing a failure; recorded failures remain failures. A matching live turn remains active, and an agent live on a later turn cannot resurrect an earlier step. The existing outside-block activity presenter path is unchanged.Verification at
036bd38c3: focused grouping + conversation-meta tests 39/39, the orphan cases inAgentSessionWorkBlock.orphaned.test.mjspass, full desktop suite 5487/0,tsc --noEmitclean,pnpm checkat baseline, and all push hooks green (including the full desktop test hook).Seven mutants pin this gate, each run in isolation against the unit suite: gate removed / always running (4 failures), gate inverted so
executingis never running (5 — the fix that would have "passed" the report while breaking live work), truthiness instead of turn ownership (1),item.turnId === liveTurnIdwithout the null guard (1), abandoned step reportedfailedinstead ofsettled(4),liveTurnIdforced null in the meta builder (2),lastTurnIdreading only the final block (1).Measured in a real browser as well, since the failure mode is presentational: an agent panic mid-step folds to
3 stepswith 0 infinite animations and rail states["settled","settled","settled"]; the same events without the panic keep the rail open at["settled","settled","running"]with exactly 1 infinite animation. Both directions are mutation-checked — removing the liveness comparison fails the orphan scenario and passes the live one, inverting it does the reverse — so no one-sided fix satisfies both.The originally-flagged file,
agentSessionToolRunSummary.ts, was deleted by this PR's retarget and does not exist at this head; the gate landed at the grouping/projection seam (agentSessionWorkBlockGrouping.ts,agentSessionConversationMeta.ts) instead. See #6536 (comment).Test file sizes
AgentSessionWorkBlock.test.mjsfirst landed at 1,146 lines. Today's desktop ratchet passes it only because the script roots allowlist.ts/.tsxand skip.mjs— the gap #6736 closes. With that rule table cherry-picked it is a real violation, and sinceallowedLineCountgrandfathers an over-ceiling base, whatever count C merges with becomes that file's permanent ceiling. So the suite is split here rather than after: rig 311, live/finished 352, orphaned 528; conversation 366, chrome 274, harness 571 — all under 1,000.The split preserves behaviour, checked rather than assumed: the 30 test titles across the two files are an exact set match with the 30 in the single file, and every body is byte-identical apart from one call.
prefersReducedMotionhad to becomesetPrefersReducedMotion(value)because ESM bindings are read-only in importers; stubbing that setter to a no-op fails exactly one test, the reduced-motion one, so it is not a flag nothing sets.Ratchet with #6736's rules cherry-picked, each run in the same shell as
git rev-parse HEAD: basefd2e01799exit 0, basemerge-base(origin/main)=db5617dd1exit 0. Negative control at the pre-split tree, same rules and base: exit 1,AgentSessionWorkBlock.test.mjs: new -> 1146 lines (allowed 1000).